//
// WebTools Library by S. Ali Tokmen
// http://code.google.com/p/web-tools/
// http://ali.tokmen.com/
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// 1. The above copyright notice and this permission notice shall be
//    included in all copies or substantial portions of the Software.
// 2. Namespaces or Class names of the Software components shall not
//    be modified.
// 3. Neither the name of the copyright holders nor the names of its
//    contributors may be used to endorse or promote products derived from
//    this Software without specific prior written permission.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Text;

using ICSharpCode.SharpZipLib.GZip;
using ICSharpCode.SharpZipLib.Zip.Compression.Streams;

namespace GoogleCode.WebTools
{
    /// <summary>
    /// HTTP Method: GET, POST, or POST with files/attachments.
    /// </summary>
    public enum HTTPRequestType
    {
        /// <summary>
        /// HTTP GET
        /// </summary>
        Get,

        /// <summary>
        /// HTTP POST with URL encoding
        /// </summary>
        Post,

        /// <summary>
        /// HTTP POST with Multipart encoding (used for transferring files)
        /// </summary>
        MultipartPost
    }

    /// <summary>
    /// HTTP Helper class. It handles:
    /// <ul>
    /// <li>HTTP GET and POST; including Multipart file uploads.</li>
    /// <li>Proxy configuration using the static HTTPWorker.Proxy object -you
    /// therefore set it once and it will be used at each request!</li>
    /// <li>Cookies across calls: each HTTPWorker instance has its own Cookie
    /// Manager; saving and restoring HTTP cookies across calls.</li>
    /// <li>HTTP compression (gzip and deflate), resulting in much smaller HTTP
    /// responses and much faster interactions. HTTP compression works both for
    /// "classic" HTTP-based Web sites and Web Services.</li>
    /// <li>Various HTTP protocol "tips and tricks", including redirections,
    /// host migrations or chunked responses.</li>
    /// </ul>
    /// Both the Cookie Manager and HTTP compression work on all versions of
    /// .NET Frameworks, including Mono and Microsoft .NET CF (Compact
    /// Framework, i.e. Mobile).
    /// </summary>
    public class HTTPWorker
    {
        /// <summary>
        /// Class for handling certificates in frameworks that have trouble
        /// handling it in some cases (Mono, mostly) which accepts all
        /// certificates. That might be a dangerous thing to do.
        /// </summary>
        private class AcceptAllPolicy : ICertificatePolicy
        {
            public bool CheckValidationResult(ServicePoint srvPoint, X509Certificate certificate,
                WebRequest request, int certificateProblem)
            {
                // Accept all HTTPS certificates
                return true;
            }
        }
        private static ICertificatePolicy certificatePolicy = null;

        // A kind of DNS cache. Used to translate www.facebook.com
        // into www.new.facebook.com, for instance
        private static Dictionary<String, String> replacementHosts = new Dictionary<String, String>();

        // Identify as Mozilla Firefox 2.0.0.17, US English version
        private const String UserAgent =
            "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.17) Gecko/20080829 Firefox/2.0.0.17";
        private const String Accept =
            "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/"
                    + "*;q=0.5";
        private const String AcceptLanguage = "en-us,en;q=0.5";
        private const String AcceptGZipEncoding = "gzip,deflate";
        private const String AcceptCharset = "utf-8,*";

        // Request
        private String requestURI;
        private HTTPRequestType requestType;
        private String getOrPostVars;
        private HttpWebRequest request;

        // Cookies
        private CookieManager cookies;

        // MIME payload for multipart POST
        private MIMEPayload mimePayload;

        // Response text
        private String rspText;

        private const int STREAM_BUFFER_SIZE = 2048;

        /// <summary>
        /// The Proxy server to use. Defaults to "no proxy".
        /// </summary>
        public static IWebProxy Proxy = null;

        /// <summary>
        /// The URL of the next request. If the request is a GET request and
        /// arguments have been added for this request, these will also be
        /// added to this GET URL.
        /// </summary>
        public String Url
        {
            get
            {
                if (this.requestURI == null)
                {
                    return null;
                }

                String requestURI = this.requestURI;
                if (this.requestType == HTTPRequestType.Get && this.getOrPostVars.Length > 0)
                {
                    if (requestURI.IndexOf("?") != -1)
                    {
                        // GET request with arguments
                        requestURI += "&" + this.getOrPostVars;
                    }
                    else
                    {
                        // GET request with no arguments
                        requestURI += "?" + this.getOrPostVars;
                    }
                }
                return requestURI;
            }

            set
            {
                if (value == null)
                {
                    this.requestURI = value;
                    return;
                }

                String target;
                int hostEnd = value.IndexOf('/', 10);

                if (hostEnd == -1)
                {
#if TRACE && !(PocketPC || WindowsCE)
                    Trace.WriteLine("Appending ending slash to URL: " + value);
#endif
                    value += '/';
                    hostEnd = value.IndexOf('/', 10);
                }

                String host = value.Substring(0, hostEnd);
                lock (replacementHosts)
                {
                    if (replacementHosts.ContainsKey(host))
                    {
                        target = replacementHosts[host] + value.Substring(hostEnd);
                    }
                    else
                    {
                        target = value;
                    }
                }

                this.requestURI = target;
            }
        }

        /// <summary>
        /// The request type. This must be set before any arguments are added
        /// to the request.
        /// </summary>
        public HTTPRequestType Type
        {
            get
            {
                return this.requestType;
            }

            set
            {
                this.requestType = value;

                switch (this.Type)
                {
                    // GET request
                    case HTTPRequestType.Get:
                        mimePayload = null;
                        getOrPostVars = "";
                        break;

                    // POST request
                    case HTTPRequestType.Post:
                        mimePayload = null;
                        getOrPostVars = "";
                        break;

                    // POST request with attachments
                    case HTTPRequestType.MultipartPost:
                        mimePayload = new MIMEPayload();
                        getOrPostVars = null;
                        break;
                }
            }
        }

        /// <summary>
        /// Creates a new request (but still remembers cookies and all), sets
        /// the Url and Type and makes sure initialization is OK.
        /// </summary>
        /// <param name="url">Url to set</param>
        /// <param name="type">Request type to set</param>
        public void SetTarget(String url, HTTPRequestType type)
        {
            this.rspText = null;
            this.mimePayload = null;

            this.Url = url;
            this.Type = type;
        }

        /// <summary>
        /// Add an argument to a POST request.
        /// </summary>
        /// <param name="key">Argument name</param>
        /// <param name="value">Argument value</param>
        public void AddValue(String key, String value)
        {
            value = Uri.EscapeUriString(value);

            if (mimePayload != null)
            {
                mimePayload.AddValue(key, value);
            }
            else
            {
                // The HTTP specifications say each argument is separated with
                // a "&", but the first argument doesn't start with it
                if (getOrPostVars.Length > 0)
                {
                    getOrPostVars += '&';
                }

                // Add the key
                getOrPostVars += key;

                // Careful with null values (which are allowed)
                if (value != null)
                {
                    getOrPostVars += '=' + value;
                }
            }
        }

        /// <summary>
        /// Add a file to a multi-part POST request.
        /// </summary>
        /// <param name="key">Argument name</param>
        /// <param name="fileName">Filename</param>
        public void AddFile(String key, String fileName)
        {
            if (mimePayload != null)
            {
                mimePayload.AddFile(key, fileName);
            }
            else
            {
                throw new Exception("You must use HTTPRequestType.MultipartPost "
                    + "if you want to submit files via HTTP POST.");
            }
        }

        /// <summary>
        /// Send the currently pending request.
        /// </summary>
        public void SendRequest()
        {
            using (Stream s = this.SendRequestAndGetResponseStream())
            {
                // SendRequestAndGetResponseStream sets rspText if necessary
                if (this.rspText == null)
                {
                    using (StreamReader sr = new StreamReader(s))
                    {
                        this.rspText = sr.ReadToEnd();
                    }
                }
            }
        }

        /// <summary>
        /// Send the currently pending request and return the decoded response
        /// stream. This method will therefore automatically decompress all
        /// compressed streams, and internally handles all cleanup work on the
        /// HTTP Stream objects.
        /// </summary>
        /// <returns>The decoded HTTP response stream</returns>
        public Stream SendRequestAndGetResponseStream()
        {
            if (this.requestURI == null)
            {
                throw new InvalidOperationException("You must set a target Url first!");
            }
            if (this.rspText != null)
            {
                throw new InvalidOperationException("Request already sent!");
            }

            // Avoid all kinds of issues with certificate validation
            if (HTTPWorker.certificatePolicy == null)
            {
                HTTPWorker.certificatePolicy = new AcceptAllPolicy();
                // Disable warning (for MS .net 2.0 and superior)
#pragma warning disable 0618
                System.Net.ServicePointManager.CertificatePolicy = HTTPWorker.certificatePolicy;
#pragma warning restore 0618
            }

            this.request = (HttpWebRequest)WebRequest.Create(this.Url);

            this.request.ProtocolVersion = HttpVersion.Version11;
            this.request.UserAgent = HTTPWorker.UserAgent;
            this.request.Accept = HTTPWorker.Accept;
            this.request.Headers["Accept-Language"] = HTTPWorker.AcceptLanguage;
            this.request.Headers["Accept-Encoding"] = HTTPWorker.AcceptGZipEncoding;
            this.request.Headers["Accept-Charset"] = AcceptCharset;
            this.request.KeepAlive = true;
            this.request.Headers["Keep-Alive"] = "300";
            this.request.Timeout = 300000;
            this.request.ReadWriteTimeout = 300000;
#if (PocketPC || WindowsCE)
            // .net CF will not let us set a null Proxy. Be careful.
            if (Proxy != null)
            {
                this.request.Proxy = Proxy;
            }
#else
            this.request.Proxy = Proxy;
#endif
            switch (this.Type)
            {
                // GET request
                case HTTPRequestType.Get:
                    this.request.Method = "GET";
                    break;

                // POST request
                case HTTPRequestType.Post:
                    this.request.Method = "POST";
                    this.request.ContentType = "application/x-www-form-urlencoded";
                    break;

                // POST request with attachments
                case HTTPRequestType.MultipartPost:
                    this.request.Method = "POST";
                    this.request.ContentType = "multipart/form-data; boundary=" + mimePayload.Boundary;
                    break;
            }

            // This is to prevent the "417" status code error from web servers
            // that don't support sending POST data in a follow-up request
            if (this.request.ServicePoint != null)
            {
                this.request.ServicePoint.Expect100Continue = false;
            }
            // Don't allow 302 auto-redirects by default
            this.request.AllowAutoRedirect = false;

#if TRACE && DEBUG && !(PocketPC || WindowsCE)
            Trace.WriteLine("HTTPWorker::SendRequestAndGetResponse for " + this.request.Method
                + " URL " + this.request.RequestUri);
#endif
            // Add previously stored cookies to the header
            this.cookies.SetCookies(this.request);

            // Create the request payload
            byte[] data = null;

            if (this.request.Method == "POST")
            {
                if (mimePayload == null)
                {
                    data = Encoding.ASCII.GetBytes(getOrPostVars);
                }
                else
                {
                    mimePayload.Finish();
                    data = mimePayload.Data;
                }

                this.request.ContentLength = data.Length;
                Stream st = this.request.GetRequestStream();
                st.Write(data, 0, data.Length);
                st.Close();
            }

            // Get the HTTP response
            using (HttpWebResponse rsp = (HttpWebResponse)this.request.GetResponse())
            {
                // Remember newly sent cookies
                this.cookies.GetCookies(rsp);
#if TRACE && DEBUG && !(PocketPC || WindowsCE)
                Trace.WriteLine("HTTPWorker::SendRequestAndGetResponse terminated for " + this.request.Method
                    + " URL " + this.request.RequestUri);
#endif
                // The Location header is set if the server wants us to be
                // redirected. Otherwise, it is null or empty.
                String location = rsp.Headers["Location"];
                if (location != null && location != this.Url.ToString())
                {
#if TRACE && !(PocketPC || WindowsCE)
                    Trace.WriteLine("HTTPWorker::SendRequestAndGetResponse for " + this.request.Method
                        + " URL " + this.request.RequestUri + " redirected to " + location);
#endif
                    Uri oldUri = new Uri(this.Url);
                    Uri newUri;
                    try
                    {
                        newUri = new Uri(location);
                    }
                    catch (UriFormatException)
                    {
                        // The new location is probably a relative URL
                        newUri = new Uri(oldUri, location);
                    }

                    if (oldUri.AbsolutePath == newUri.AbsolutePath)
                    {
                        // Path is the same, this is a host name change;
                        // therefore cache the change
                        lock (replacementHosts)
                        {
                            replacementHosts[oldUri.Host] = newUri.Host;
                        }

                        // Redirect
                        SetTarget(newUri.ToString(), HTTPRequestType.Get);
                        return this.SendRequestAndGetResponseStream();
                    }
                    else
                    {
                        // Asked for redirect without the same path,
                        // let the caller handle all details
                        this.SetTarget(newUri.ToString(), HTTPRequestType.Get);
                        this.rspText = "<script>window.location.href = \"" + newUri.ToString() + "\"</script>";
                    }
                }

                // Get the original response stream and the decoder response stream
                Stream responseStream = rsp.GetResponseStream();
                Stream decoderStream = null;
                if (rsp.ContentEncoding == "gzip")
                {
                    decoderStream = new GZipInputStream(rsp.GetResponseStream());
                }
                else if (rsp.ContentEncoding == "deflate")
                {
                    decoderStream = new InflaterInputStream(rsp.GetResponseStream());
                }
                else
                {
                    decoderStream = responseStream;
                }

                using (responseStream)
                {
                    using (decoderStream)
                    {
                        MemoryStream decodedStream = new MemoryStream();
                        byte[] buffer = new byte[STREAM_BUFFER_SIZE];

                        while (true)
                        {
                            int readBytes = decoderStream.Read(buffer, 0, STREAM_BUFFER_SIZE);
                            if (readBytes > 0)
                            {
                                decodedStream.Write(buffer, 0, readBytes);
                            }
                            else
                            {
                                break;
                            }
                        }

                        decodedStream.Seek(0, SeekOrigin.Begin);
                        return decodedStream;
                    }
                }
            }
        }

        /// <summary>
        /// Cancel the current HTTP request.
        /// </summary>
        public void CancelRequest()
        {
            if (this.request != null)
            {
                this.request.Abort();
            }
        }

        /// <summary>
        /// If neccessary sends the HTTP request and returns the response.
        /// </summary>
        public String ResponseText
        {
            get
            {
                if (this.rspText == null)
                {
                    this.SendRequest();
                }
                return this.rspText;
            }
        }

        /// <summary>
        /// Constructor that copies all cookies from another HTTPWorker.
        /// </summary>
        /// <param name="worker">HTTPWorker to copy cookies from.</param>
        public HTTPWorker(HTTPWorker worker)
        {
            this.Url = null;
            this.Type = HTTPRequestType.Get;

            if (worker == null)
            {
                this.cookies = new CookieManager();
            }
            else
            {
                this.cookies = new CookieManager(worker.cookies);
            }
        }

        /// <summary>
        /// Creates an HTTPWorker with an empty cookie container.
        /// </summary>
        public HTTPWorker()
            : this(null)
        {
            // Other constructor handles everything
        }
    }
}
